Skip to content

Conversation

@AtticusKuhn
Copy link
Contributor

This PR adds a pass to circt-opt called --convert-core-to-fsm. This pass converts code written in RTL (comb + seq) and converts it to the fsm dialect.

It recognises and extracts FSM structure from an RTL netlist. It has similar functionality to fsm_extract in yosys:
https://yosyshq.readthedocs.io/projects/yosys/en/0.46/cmd/fsm.html

@AtticusKuhn AtticusKuhn requested a review from darthscsi as a code owner August 14, 2025 14:07
@AtticusKuhn AtticusKuhn marked this pull request as draft August 14, 2025 14:07
Copy link
Contributor

@TaoBi22 TaoBi22 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for making the PR! I haven't properly reviewed the pass code yet (I'll try to do that soon) but in the meantime I have some high-level comments

Copy link
Contributor

@TaoBi22 TaoBi22 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Still haven't done a detailed pass, just some things I noticed)

@TaoBi22 TaoBi22 changed the title [Feat][circt-opt] Add pass --convert-core-to-fsm [circt-opt] Add pass --convert-core-to-fsm Aug 14, 2025
Comment on lines +387 to +399
moduleOp.walk([&](circt::seq::CompRegOp reg) {
if (reg.getName()->contains("state")) {
stateRegs.push_back(reg);
} else {
variableRegs.push_back(reg);
}
});
if (stateRegs.empty()) {
emitError(moduleOp.getLoc())
<< "Cannot find state register in this FSM. You might need to "
"manually specify which registers are state registers.\n";
return mlir::failure();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This analysis does not look reliable. Is it possible to replace it with an analogue of fsm_detect from yosys?

Copy link
Contributor Author

@AtticusKuhn AtticusKuhn Aug 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's true, but I think that to have modularity, it would be fine to split up the total pipeline into fsm_detect and fsm_extract (like Yosys does). So, theoretically there could be a pass circt-opt --annotate-fsm-state-registers that works in combination with circt-opt --convert-core-to-fsm. In the meantime, it's not too difficult for the user to manually annotate the FSM state registers in an RTL design.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe for now we could just require it to always have the name of the state register explicitly given by the user? That way we don't need to worry about users having it opaquely pick up a register they don't expect, and we can always later change it to a flag that will get it to look for an attribute

@AtticusKuhn AtticusKuhn marked this pull request as ready for review August 15, 2025 15:39
Atticus Kuhn added 2 commits August 27, 2025 16:16
Copy link
Contributor

@TaoBi22 TaoBi22 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the big delay in getting time to properly review this @AtticusKuhn! If you don't have time to work towards getting this in then let me know and we can work something out.

I've left some comments below - the additional thing is that for a new pass we need to add some FileCheck/verify-diagnostics tests, both to make sure it works and to make sure we see errors when we expect to. You should find examples of this on other PRs that added new passes, but let me know if you have any questions about it!

#include "llvm/Support/LogicalResult.h"
#include "llvm/Support/raw_ostream.h"

#include "circt/Conversion/CoreToFSM.h"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Repeated include (I'm surprised clang-format didn't catch this - I suspect it's due to having a space in the middle of the include list, which it would also be nice to drop)

size_t currentValue);

static void getPossibleValues(llvm::DenseSet<size_t> &possibleValues, Value v) {
if (circt::hw::ConstantOp c =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
if (circt::hw::ConstantOp c =
if (ConstantOp c =

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually can probably just be an auto?

possibleValues.insert(c.getValueAttr().getValue().getZExtValue());
return;
}
if (circt::comb::MuxOp m = dyn_cast_or_null<comb::MuxOp>(v.getDefiningOp())) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above

llvm::DenseSet<size_t> &finalPossibleValues, size_t operandIdx,
size_t currentValue);

static void getPossibleValues(llvm::DenseSet<size_t> &possibleValues, Value v) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the name of this function makes it sound like a getter, but it's a void. Maybe addPossibleValues or something would make more sense?

Comment on lines +89 to +93
if (numStates > 256) { // Heuristic threshold
// If the search space is too large, we abandon the analysis for this
// path. The outer function will fall back to its own full-range
// default.
return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to emit a warning/error here?

mlir::Block &newGuardBlock = guardRegion.front();
Operation *terminator = newGuardBlock.getTerminator();
hw::OutputOp hwOutputOp = dyn_cast<hw::OutputOp>(terminator);
assert(hwOutputOp && "Expected terminator to be hw.OutputOp");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert(hwOutputOp && "Expected terminator to be hw.OutputOp");
assert(hwOutputOp && "Expected terminator to be hw.output op");

reg.erase();
}
Operation *terminator = actionRegion.back().getTerminator();
hw::OutputOp hwOutputOp = dyn_cast<hw::OutputOp>(terminator);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This cast looks unnecessary

clonedRegOp->erase();
}

// delete the arguments from the output block
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment looks like it's describing something other than what follows it

Comment on lines +850 to +853
if (failed(converged)) {
stateOp.emitError("Failed to canonicalize the generated state op");
return failure();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like we can probably catch this after the first call directly?

hw::ConstantOp guardConst =
transition.getGuardReturn()
.getOperand()
.getDefiningOp<circt::hw::ConstantOp>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this guaranteed not to return null?

@AtticusKuhn
Copy link
Contributor Author

Thanks for the feedback @TaoBi22 . I'm somewhat busy currently, but I'll be sure to work through and address your feedback in the coming days.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants